iT邦幫忙

2026 iThome 鐵人賽

DAY 1
0
AI 自動化

讓 Claude 學會看圖:30 天打造 LLM 驅動的機器視覺工作站(MCP × OpenCV × Terminal Plugin)》系列 第 2

# Day 2|Hyper Terminal 插件機制:在 Electron renderer 裡動手術

  • 分享至 

  • xImage
  •  

Day 2|Hyper Terminal 插件機制:在 Electron renderer 裡動手術

Day 2 封面:在終端機裡動手術

昨天講完「為什麼」,今天開始講「怎麼做」。第一個問題是:要在哪裡蓋這個開發者主控台?

為什麼是 Hyper,而不是自己刻一個 Electron app?

一開始我認真考慮過自己開一個 Electron 專案:自己管視窗、自己嵌 xterm.js、自己接 node-pty。但很快就打消念頭——光是「把 PTY 做到跟系統 shell 完全一致」這件事,就是一個無底洞:ConPTY 的相容性、視窗 resize 時的 reflow、複製貼上行為、IME 輸入……這些 Hyper 全都解完了。

Hyper 是 Vercel 出的 Electron 終端機,整個 UI 是 React + Redux,而且它有一個殺手級特性:插件可以攔截、包裝、改寫它的每一層。與其重造終端機,不如把 Hyper 當成「已經幫你把 PTY 搞定的 Electron 底座」,我們只負責在上面長出 IDE。

Hyper 的兩個世界

跟所有 Electron app 一樣,Hyper 分成兩個行程。插件的程式碼整包跑在 Renderer——它不是獨立行程,而是被 require() 進 Hyper 的 React 世界裡:

(Hyper 的兩個世界與插件掛載點)

要載入插件,只需要在 ~/.hyper.js 加一行:

module.exports = {
  config: { /* ... */ },
  localPlugins: ['C:/path/to/C-Console/devterm-plugin'],
};

Hyper 會去該目錄找 package.jsonmain 欄位——我們指向 webpack 打包出來的 dist/index.js。build 設定的重點只有兩個:

// webpack.config.js(節錄)
module.exports = {
  target: 'electron-renderer',   // 讓 require('electron') / fs 都能用
  output: { libraryTarget: 'commonjs2' }, // Hyper 用 CommonJS require 插件
};

target: 'electron-renderer' 是關鍵:插件因此同時擁有瀏覽器 DOMNode.js API(fspathipcRenderer)。後面所有功能——檔案樹、git 操作、監看 state.json——都建立在這個「雙棲」能力上。

插件掛載點:decorate 家族

Hyper 插件本質上是一個「匯出特定名字函式」的 CommonJS 模組。DevTerminal 實際用到這幾個:

exports.decorateConfig = (config) => { /* 改設定、注入 CSS */ };
exports.decorateHyper  = (Hyper, { React }) => { /* 包 UI 最外層 */ };
exports.decorateTerm   = (Term, { React })  => { /* 包每個終端機 */ };
exports.middleware     = (store) => (next) => (action) => { /* 攔 Redux action */ };

其中最重要的是 decorateHyper:Hyper 把它的根元件交給你,你回傳一個新的元件類別把它包起來。這就是「動手術」的手術口——三欄式佈局就是在這裡縫進去的:

decorateHyper 把三欄佈局包在 Hyper 外面
實際的程式碼(節錄自 src/index.js):

exports.decorateHyper = (Hyper, { React: R }) => {
  return class DevTermHyper extends R.Component {
    render() {
      const { editFile, visionOpen } = this.state;
      return R.createElement(
        'div',
        { style: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 } },
        R.createElement(Hyper, { ...this.props }),        // 原本的終端機
        R.createElement(Sidebar, { cwd: this.state.cwd }), // 我們的側邊欄
        editFile && R.createElement(EditorPane, { file: editFile }),
        R.createElement(StatusBar, { /* ... */ }),
      );
    }
  };
};

第一個踩坑:CSS 大戰是打不贏的

包好元件之後,下一步是把 Hyper 本體「擠」到右邊 65%。直覺做法是注入 CSS:

.hyper_main { left: 260px !important; }

沒用。 Hyper 對 .hyper_main 用的是行內樣式position: fixed,而且它自己也在動態改 style。CSS specificity 這場仗,樣式表永遠打不贏行內樣式。

最後有效的解法是放棄 CSS,直接動 DOM:

// updateLayout():行內 !important 永遠壓過樣式表的 !important
const main = document.querySelector('.hyper_main');
main.style.setProperty('left', `${SIDEBAR_WIDTH}px`, 'important');
main.style.setProperty('bottom', `${STATUSBAR_HEIGHT}px`, 'important');

element.style.setProperty(prop, value, 'important') 是整個佈局系統能動的基石。之後每次開關編輯器 / Vision 面板,都是重新跑一次這個函式,用 requestAnimationFrame 等 React 畫完再調 DOM。

狀態管理:一個物件就夠了

最後一塊拼圖:側邊欄、編輯器、狀態列之間怎麼共享狀態?答案簡單到有點反高潮——一個 mutable 物件加一組 listeners:

pluginState 極簡狀態模型

const pluginState = {
  cwd: null, editFile: null, visionState: null, /* ... */
  listeners: new Set(),
  notify() { this.listeners.forEach(fn => fn(this)); },
};

任何模組(CWD 偵測、檔案樹點擊、vision watcher)改完欄位就呼叫 notify();DevTermHypercomponentDidMount 註冊 listener,把欄位抄進 this.state 觸發重繪。沒有 Redux、沒有 Context——插件這個規模,pub/sub 就夠了,而且除錯時打開 console 直接看得到全部狀態。

小結

今天我們搞清楚了三件事:

  1. 插件跑在 Renderer,同時擁有 DOM + Node.js 能力(target: 'electron-renderer')
  2. decorateHyper 是手術口:回傳新元件把 <Hyper> 包進三欄佈局
  3. 佈局要用 setProperty(..., 'important') 直接動 DOM,CSS 大戰打不贏

明天(Day 3)深入佈局細節:三欄的尺寸計算、開關編輯器時的 relayout 時序,以及為什麼要 requestAnimationFrame



上一篇
Day 1|為什麼要讓 LLM 當視覺工程師?——系列開賽與架構總覽
下一篇
Day 3|三欄式佈局:Sidebar / Editor / Terminal,與 inline style 的戰爭
系列文
讓 Claude 學會看圖:30 天打造 LLM 驅動的機器視覺工作站(MCP × OpenCV × Terminal Plugin)》3
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言